// === External dependence ===
const PATH_SIGNS = {
    CLOSURE: 'closure',
    CONDITION: 'condition',
    RELATIVESPEED: 'relativeSpeed',
    STOP: 'stop',
    SPOTTURN: 'spotTurn',
    PROHIBITION: 'prohibition',
    PERMISSION: 'permission',
    BRIDGE: 'bridge'
};
const Expert = {
    checkBit(bitmask, bit) {
        if (!bitmask) return;
        let byteIndex = Math.floor(bit / 8),
            bitIndex = bit - byteIndex * 8;
        bitmask = bitmask.constructor === Array ? bitmask[byteIndex] : bitmask;
        return (bitmask & (1 << bitIndex)) !== 0;
    },
    getNumberFromByteArray(byteArray, startIndex, length) {
        if (!byteArray) return;
        let number = 0,
            byteIndex = startIndex / 8;
        while ((length -= 8) >= 0) {
            number |= (byteArray[byteIndex++] & 0xff) << length;
        }
        return number;
    }
};
const Color = {
    rgbToHex: function (color) {
        let reg = /^(rgba|RGBA|rgb|RGB)/;
        if (reg.test(color)) {
            let strHex = '#';
            // => Array
            let colorArr = color.replace(/(?:\(|\)|rgba|RGBA|rgb|RGB)*/g, '').split(',');
            // => hex
            for (let i = 0; i < 3; i++) {
                let hex = Number(colorArr[i]).toString(16);
                if (hex === '0') hex += hex;
                strHex += hex;
            }
            return strHex;
        } else {
            return color;
        }
    },

    hexToRgb: function (color) {
        let reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;
        color = color.toLowerCase();
        if (reg.test(color)) {
            // #fff => #ffffff
            if (color.length === 4) {
                let colorNew = '#';
                for (let i = 1; i < 4; i += 1) {
                    colorNew += color.slice(i, i + 1).concat(color.slice(i, i + 1));
                }
                color = colorNew;
            }
            // #ffffff => rgb
            let colorChange = [];
            for (let i = 1; i < 7; i += 2) {
                colorChange.push(parseInt('0x' + color.slice(i, i + 2)));
            }
            return 'rgb(' + colorChange.join(',') + ')';
        } else {
            return color;
        }
    },

    colorToByteArray: function (color) {
        if (!color || color === '?') return [0, 0, 0, 0];
        color = this.hexToRgb(color);
        color = color.replace(/\s/g, '');
        let values = /^rgba\(([\d]+),([\d]+),([\d]+),([\d]+|[\d]*.[\d]+)\)/.exec(color);
        if (!values) values = /^rgb\(([\d]+),([\d]+),([\d]+)\)/.exec(color);
        if (!values) return [0, 0, 0, 0];

        values.shift();
        values[3] = values.length < 4 ? 255 : Math.round(values[3] * 255);
        return values;
    },
    getContrastColor: function (color) {
        if (!color) return 'black';
        var values = this.colorToByteArray(color);
        var brightness = (values[0] * 299 + values[1] * 587 + values[2] * 114) / 1000;
        brightness += ((255 - brightness) * (255 - values[3])) / 255; // consider alpha channel
        // return brightness > 125 ? 'black' : 'white';
        return brightness > 125 ? '#363433' : '#f1f0ed';
    },

    lightOrDark: function (col) {
        let rtn = 'light';
        if (!col) return rtn;
        let r, g, b;
        if (/^#/g.test(col)) {
            r = Math.floor(parseInt(col.slice(1, 3), 16) * 0.299);
            g = Math.floor(parseInt(col.slice(3, 5), 16) * 0.578);
            b = Math.floor(parseInt(col.slice(5, 7), 16) * 0.114);
            // if (r + g + b >= 192) rtn = 'light';
            if (r + g + b >= 150) rtn = 'light';
            else rtn = 'dark';
        }
        // console.error('rbg:', col, r, b, g, amt);
        return rtn;
    },

    lightenDarkenColor: function (col, amt) {
        if (amt >= 1) return col;
        let r, g, b;
        if (/^#/g.test(col)) {
            r = Math.floor(amt * parseInt(col.slice(1, 3), 16) + (1 - amt) * 255);
            g = Math.floor(amt * parseInt(col.slice(3, 5), 16) + (1 - amt) * 255);
            b = Math.floor(amt * parseInt(col.slice(5, 7), 16) + (1 - amt) * 255);
        }
        // console.error('rbg:', col, r, b, g, amt);
        if (r && g && b) return '#' + r.toString(16).slice(-2) + g.toString(16).slice(-2) + b.toString(16).slice(-2);
    }
};
class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }

    round() {
        this.x = Math.round(this.x);
        this.y = Math.round(this.y);
        return this;
    }

    equals(point) {
        if (this.x !== point.x) return false;
        return this.y === point.y;
    }

    distance(point) {
        let dx = this.x - point.x,
            dy = this.y - point.y;
        return Math.sqrt(dx * dx + dy * dy);
    }

    middle(point) {
        let x = (this.x + point.x) / 2,
            y = (this.y + point.y) / 2;
        return new Point(x, y);
    }

    angle(point) {
        let angle = Point.getDegrees(Math.atan2(this.y - point.y, this.x - point.x));
        if (angle < 0) angle += 360;
        if (angle > 360) angle -= 360;
        return angle;
    }

    rotate(point, angle) {
        angle = Point.getRadians(angle);
        let x1 = point.x + (this.x * Math.cos(angle) - this.y * Math.sin(angle)),
            y1 = point.y + (this.y * Math.cos(angle) + this.x * Math.sin(angle));
        return new Point(x1, y1);
    }

    isContained(points) {
        for (let i = 0; i < points.length; i++) {
            if (points[i].equals(this)) return true;
        }
        return false;
    }

    static equals(p1, p2) {
        if (p1.x !== p2.x) return false;
        return p1.y === p2.y;
    }

    static getDegrees(value) {
        value *= 180 / Math.PI;
        if (value < 0) value += 360;
        if (value > 360) value -= 360;
        return value;
    }

    static getRadians(value) {
        return value * (Math.PI / 180);
    }

    static getAngleDelta(a1, a2) {
        let delta = Math.abs(a1 - a2);
        if (delta > 180) delta = Math.abs(delta - 360);
        return delta;
    }
    static getBezierPoint(i, cp) {
        let cx = 3 * (cp[1].x - cp[0].x),
            bx = 3 * (cp[2].x - cp[1].x) - cx,
            ax = cp[3].x - cp[0].x - cx - bx,
            cy = 3 * (cp[1].y - cp[0].y),
            by = 3 * (cp[2].y - cp[1].y) - cy,
            ay = cp[3].y - cp[0].y - cy - by;
        let x = ax * Math.pow(i, 3) + bx * Math.pow(i, 2) + cx * i + cp[0].x,
            y = ay * Math.pow(i, 3) + by * Math.pow(i, 2) + cy * i + cp[0].y;
        return new Point(x, y);
    }

    static getClothoidPoints(list) { //eslint-disable-line
        let points = [];
        for (let i = 0; i < list.length - 3; i += 3) {
            let p0 = new Point(list[i].x, list[i].y),
                p1 = new Point(list[i + 1].x, list[i + 1].y),
                p2 = new Point(list[i + 2].x, list[i + 2].y),
                p3 = new Point(list[i + 3].x, list[i + 3].y);

            let estimatedLength = 0;
            estimatedLength += p0.distance(p1);
            estimatedLength += p1.distance(p2);
            estimatedLength += p2.distance(p3);

            let steps = Math.min(0.5, 1 / (estimatedLength / 100)); // each 100 mm or smaller if necessary
            for (let p = steps; p < 1; p += steps) {
                let cx = 3 * (p1.x - p0.x),
                    bx = 3 * (p2.x - p1.x) - cx,
                    ax = p3.x - p0.x - cx - bx,
                    cy = 3 * (p1.y - p0.y),
                    by = 3 * (p2.y - p1.y) - cy,
                    ay = p3.y - p0.y - cy - by,
                    x = ax * Math.pow(p, 3) + bx * Math.pow(p, 2) + cx * p + p0.x,
                    y = ay * Math.pow(p, 3) + by * Math.pow(p, 2) + cy * p + p0.y;
                points.push(new Point(x, y));
            }
        }
        points.unshift(new Point(list[0].x, list[0].y));
        points.push(new Point(list[list.length - 1].x, list[list.length - 1].y));
        return points;
    }

    static getPointerPagePos(e) {
        let pageX = e.type.indexOf('touch') < 0 ? e.pageX : e.touches[0].pageX,
            pageY = e.type.indexOf('touch') < 0 ? e.pageY : e.touches[0].pageY;
        return new Point(pageX, pageY);
    }

    static getIsometricPos(x, y, flipY = false) {
        if (flipY) y = -y;
        let angle = Point.getRadians(30);
        let gx = Math.floor((x - y) * Math.cos(angle)),
            gy = Math.floor((x + y) * Math.sin(angle));
        // let gx = x * Math.cos(angle) - y * Math.sin(angle),
        //     gy = y * Math.cos(angle) + x * Math.sin(angle);
        // let gx = x + (x * Math.cos(angle) - y * Math.sin(angle)),
        //     gy = y + (y * Math.cos(angle) + x * Math.sin(angle));
        if (flipY) gy = -gy;
        return new Point(gx, gy);

        // return new Point(x, y).rotate(new Point(x, y), 30);
    }
    static getPosFromIsometric(x, y) {
        let angle = Point.getRadians(30);
        let wx = Math.floor((x * Math.sin(angle) + y * Math.cos(angle)) / (2 * Math.sin(angle) * Math.cos(angle))),
            wy = Math.floor((y * Math.cos(angle) - x * Math.sin(angle)) / (2 * Math.sin(angle) * Math.cos(angle)));
        return new Point(wx, wy);
    }
}
// ===External dependence END ===


let connection = null,
    connectTime = Date.now(),
    serverTime = Date.now(),
    lastServerTime = Date.now(),
    lastMessageTime = Date.now(),
    workTime = Date.now();
// let reloadTag = false;
let timer = null;
let resources = {};
let refreshTypes = {}; // refresh after websocket update
let reQueueTypes = {}; // refresh after queue
// =================== initLocalDate
let virtualLifecycleTimer = null,
    virtualLifecycleDelay = 100,
    virtualVehiclesStep = 1000;

/**
 * onmessage
 * msg: { task, token, subscriptions, lifeCyclePeriod, offline }
 * @param msg
 */
onmessage = function (msg) {
    let res = msg.data; 
    console.warn('[kernel.worker] onmessage offline: ', res.offline);
    console.warn('[kernel.worker] onmessage task: ', res.task);
    // if (res.offline) return initLocalDate({ vehiclesLimit: 1000, offline: res.offline });
    if (res.offline) return initLocalDate({ virtualVehicles: res.virtualVehicles, res: res.resources });
    else if (res.task === 'connect') handleConnect(res);
    else if (res.task === 'click') handleRequeue(res);
    else if (res.task === 'send') handleSend(res.data);
    else if (res.task === 'terminate') handleTerminate(res.data);
};
// ===================== methods
function handleTerminate() {
    if (connection) connection.close();
    if (timer) clearInterval(timer);
    // if (virtualLifecycleTimer) cancelAnimationFrame(virtualLifecycleTimer);
    if (virtualLifecycleTimer) clearTimeout(virtualLifecycleTimer);
    if (this && typeof this.close === 'function') this.close();
    resources = {};
    connectTime = Date.now();
    serverTime = Date.now();
    workTime = Date.now();
    // reloadTag = false;
    console.warn('[kernel.worker] handleTerminate: ', connection, resources);
}

// =================== initLocalDate
function initLocalDate({ virtualVehicles = 0, res }) {
    if (virtualLifecycleTimer) {
        // handleTerminate();
        // setTimeout(() => initLocalDate({ vehiclesLimit, offline }), 100);
        clearTimeout(virtualLifecycleTimer);
        resources = {};
        connectTime = Date.now();
        serverTime = Date.now();
        workTime = Date.now();
        // initLocalDate({ vehiclesLimit, offline });
        // return;
    }
    // handleTerminate();
    // console.warn('[kernel] initLocalDate...', virtualVehicles, res);
    resources = res;
    if (!resources['vehicles']) resources['vehicles'] = [];
    refreshTypes = { contours: true, sements: true };
    postMessage({ task: 'resources', data: resources, refreshTypes }); // 全量

    for (let i = 0; i < virtualVehicles; i++) {
        let path = getRandomPath(),
            mod = getRandomModel();
        // let controlPoints = JSON.parse(JSON.stringify(path.controlPoints)),
        let controlPoints = path.controlPoints,
            rp = controlPoints.length && controlPoints[0];
        // console.error('rp: ', rp, rp.x, rp.y);
        let t = {
            // id: i,
            // id: resources['models'][midx]['vehicleKey'] + i,
            id: mod['vehicleKey'] + i,
            idx: i,
            isOnline: true,
            isValidated: true,
            remoteId: 'FREE',
            alertColor: [],
            controlPoints,
            tunedPos: {
                x: rp.x,
                y: rp.y,
                h: 0
            },
            route: [path.id],
            routeExt: path,
            properties: {}
        };
        resources['vehicles'].push(t);
        let idx = resources['vehicles'].length - 1;
        vehicleCreate(idx);
    }
    console.info('[kernel.worker] resources: ', resources, this);
    serverTime = Date.now();
    // virtualLifecycleTimer = requestAnimationFrame(virtualLifecycle);
    virtualLifecycleTimer = setTimeout(virtualLifecycle, virtualLifecycleDelay);
}
function getRandomModel() {
    let midx = Math.floor(Math.random() * resources['models'].length);
    return resources['models'][midx];
}
function getRandomPath() {
    // let paths = getUsefulPaths(true),
    let paths = getUsefulPaths(),
        ridx = Math.floor(Math.random() * paths.length),
        path = paths[ridx];
    return JSON.parse(JSON.stringify(path));
}
function getUsefulPaths(straightOnly = false) { //eslint-disable-line
    if (!resources || !resources['paths']) return [];
    // return resources['paths'].filter(s => s.id === '1033');
    // return resources['paths'].filter(s => s.id === '994');
    if (straightOnly) return resources['paths'].filter(s => s.controlPoints.length === 2 && s.length >= 50000);
    else return resources['paths'].filter(s => s.length > virtualVehiclesStep * 5);
}
function isTwoPointEquals(p1, p2) { //eslint-disable-line
    return !(p1.x !== p2.x || p1.y !== p2.y);
}
function getNextPoint(vehicle) { //eslint-disable-line
    let nextPoint = (vehicle.controlPoints && vehicle.controlPoints[1]) || { x: 0, y: 0 };
    let dic = 0,
        x = vehicle.tunedPos.x,
        y = vehicle.tunedPos.y,
        h = 0,
        dis = Math.sqrt(Math.pow(Math.abs(nextPoint.x - x), 2) + Math.pow(Math.abs(nextPoint.y - y), 2));
    if (nextPoint.x > x) dic = 1;
    else if (nextPoint.x < x) dic = 3;
    else if (nextPoint.y > y) dic = 0;
    else if (nextPoint.y < y) dic = 2;

    // // 斜线校验，会错位，闪。。。
    // if ((vehicle.dicExtTmp === 1 && dic === 3) || (vehicle.dicExtTmp === 0 && dic === 2)) {
    //     x = nextPoint.x;
    //     y = nextPoint.y;
    //     let path = getNextPath(vehicle.routeExt);
    //     if (path) {
    //         // console.warn('[kernel] getVirtualPosition: ', path.id);
    //         vehicle.controlPoints = path.controlPoints;
    //         vehicle.routeExt = path;
    //         vehicle.route = [path.id];
    //         return { x, y, h };
    //     }
    // }
    // vehicle.dicExtTmp = dic;
    //
    // if (isNaN(dis) || dis <= virtualVehiclesStep * 2) {
    //     // console.warn('* ', dis, vehicle.controlPoints, nextPoint.x, t);
    //     let tmp = vehicle.controlPoints.splice(0, 1)[0];
    //     vehicle.controlPoints.push(tmp);
    // }
    // console.log('* ', x, y, ' => ', nextPoint.x, nextPoint.y, ' -> ', dic, ', dis: ', dis);

    if (
        isNaN(dis) ||
        dis <= virtualVehiclesStep * 2 ||
        (vehicle.dicExtTmp === 1 && dic === 3) ||
        (vehicle.dicExtTmp === 0 && dic === 2)
    ) {
        let path = getNextPath(vehicle.routeExt);
        if (path) {
            // console.warn('[kernel] getVirtualPosition: ', path.id);
            vehicle.controlPoints = path.controlPoints;
            vehicle.routeExt = path;
            vehicle.route = [path.id];
            return { x, y, h };
        }
    }
    vehicle.dicExtTmp = dic;

    if (dic === 0) {
        y = vehicle.tunedPos.y + virtualVehiclesStep + (virtualVehiclesStep * 10) / (vehicle.idx + 1);
        h = 90;
    } else if (dic === 1) {
        x = vehicle.tunedPos.x + virtualVehiclesStep + (virtualVehiclesStep * 10) / (vehicle.idx + 1);
        h = 0;
    } else if (dic === 2) {
        y = vehicle.tunedPos.y - virtualVehiclesStep - (virtualVehiclesStep * 10) / (vehicle.idx + 1);
        h = 270;
    } else if (dic === 3) {
        x = vehicle.tunedPos.x - virtualVehiclesStep - (virtualVehiclesStep * 10) / (vehicle.idx + 1);
        h = 180;
    } else {
        x = 0;
        y = 0;
        h = 0;
        vehicle.controlPoints = null;
    }
    return { x, y, h };
}
let CLOTHOIDS = {}; //store every path clothoid points
function getVirtualPosition(vehicle) { //eslint-disable-line
    let x = vehicle.tunedPos.x,
        y = vehicle.tunedPos.y,
        h = 0;
    if (vehicle.controlPoints.length === 2) return getNextPoint(vehicle);
    // if (vehicle.controlPoints.length === 2) return { x: 0, y: 0, h: 0 };
    else {
        if (!CLOTHOIDS[vehicle.routeExt.id])
            CLOTHOIDS[vehicle.routeExt.id] = Point.getClothoidPoints(vehicle.controlPoints);
        vehicle.stepExt = vehicle.stepExt ? vehicle.stepExt + 1 : 1;
        // console.warn('[kernel] getVirtualPosition: ', CLOTHOIDS[vehicle.routeExt.id]);
        let p = CLOTHOIDS[vehicle.routeExt.id][vehicle.stepExt];
        if (!p) {
            vehicle.stepExt = 0;
            let path = getNextPath(vehicle.routeExt);
            if (path) {
                // console.warn('[kernel] getVirtualPosition: ', path.id);
                vehicle.controlPoints = path.controlPoints;
                vehicle.routeExt = path;
                vehicle.route = [path.id];
            }
            // vehicle.controlPoints.reverse();
            // CLOTHOIDS[vehicle.routeExt.id].reverse();
            // // console.warn('[kernel] getVirtualPosition: ', CLOTHOIDS[vehicle.routeExt.id], vehicle.stepExt);
            return getVirtualPosition(vehicle);
        }
        x = p.x;
        y = p.y;
        // let cp = new Point(vehicle.tunedPos.x, vehicle.tunedPos.y);
        // h = Math.round(cp.angle(p));
        let cp = new Point(p.x, p.y);
        h = Math.round(cp.angle(vehicle.tunedPos));
        // console.warn('[kernel] getVirtualPosition: ', cp, p, h);
    }
    return { x, y, h };
}
function getNextPath(path) { //eslint-disable-line
    let p = path.controlPoints[path.controlPoints.length - 1],
        po = new Point(p.x, p.y);
    let s = resources['paths'].find(o => o.id !== path.id && po.equals(o.controlPoints[0]));
    return s;
}
function virtualLifecycle() {
    let now = Date.now();
    lastServerTime = serverTime ? serverTime : now;
    lastMessageTime = now;
    serverTime = now;
    workTime = serverTime - lastServerTime;
    let ws = {
        timeDelta: workTime,
        workTime: workTime,
        serverClock: workTime,
        serverTime: serverTime
    };
    postMessage({ task: 'ws', data: { ws, d: Math.floor(Math.random() * 1000) } }); // 服务器时间状态

    resources['vehicles'].forEach((t, idx) => {
        /** follow the path's controlPoints ... */
        // let { x, y, h } = getNextPoint(t);
        let { x, y, h } = getVirtualPosition(t);
        t.tunedPos = { x, y, h };
        // let idx = resources['vehicles'].findIndex(v => v.id === t.id);
        // resources[listName][idx][p] = list[i][p];
        vehicleUpdateProperty(idx, 'tunedPos', { x, y, h });
    });

    // random scripts
    let rdm = Math.floor(Math.random() * 20);
    resources.scripts = [];
    for (let i = 10, len = rdm; i < len; i++) resources.scripts.push({ id: i, properties: { activation: 'true' } });
    // random events
    rdm = Math.floor(Math.random() * 10);
    resources.events = [];
    for (let i = 0, len = rdm; i < len; i++) resources.events.push({ id: i, properties: { isNow: 'true' } });
    // random jobs
    if (!resources.jobs || !resources.jobs.length) {
        rdm = Math.floor(Math.random() * 60);
        resources.jobs = [];
        for (let i = 30, len = rdm; i < len; i++) resources.jobs.push({ id: i, vehicleId: i });
    }

    refreshTypes = { vehicles: true, scripts: true, events: true, jobs: true };
    let rtn = {};
    refreshTypes = Object.assign(refreshTypes, reQueueTypes);
    Object.keys(resources).forEach(k => (rtn[k] = refreshTypes[k] ? resources[k] : null));
    postMessage({ task: 'increment', data: rtn, refreshTypes });
    reQueueTypes = {};
    virtualLifecycleTimer = setTimeout(virtualLifecycle, virtualLifecycleDelay);
}

// ===================== handleRequeue methods
/**
 * 点击时调整资源顺序，处理覆盖问题：
 * 选中：移到最后
 * 取消：移到最前
 */
function handleRequeue(res) {
    // console.warn('[kernel.worker] handleRequeue: ', res);
    let idx = resources[res.resourcesType].findIndex(o => o.id === res.resourcesId);
    // console.warn('[kernel.worker] handleRequeue: ', idx);
    if (idx < 0) return;
    let lastIdx = resources[res.resourcesType].length - 1 || 0,
        o = resources[res.resourcesType].splice(idx, 1)[0];
    if (idx === lastIdx) resources[res.resourcesType].unshift(o);
    else resources[res.resourcesType].push(o);

    reQueueTypes[res.resourcesType] = true;
    // let tidx = resources[res.resourcesType].findIndex(o => o.id === res.resourcesId);
    // console.warn('[kernel.worker] handleRequeue: ', tidx);
}

// ===================== websocket methods
function handleConnect({ url, token, subscriptions, lifeCyclePeriod }) {
    // handleTerminate();
    connectTime = Date.now();
    lastServerTime = Date.now();
    lastMessageTime = Date.now();
    serverTime = null;
    workTime = Date.now();
    if (connection) connection.close();
    if (timer) clearInterval(timer);

    let scheme = location.protocol === 'https:' ? 'wss://' : 'ws://',
        // host = process.env.VUE_APP_WEBSOCKET_URL || location.host,
        host = url,
        ws = scheme + host + '/ws/' + token;
    connection = new WebSocket(ws); // just for local dev, easy to proxy ...

    connection.onopen = () => {
        console.warn('[kernel.worker] websocket opend!');
        connectTime = Date.now();
    };
    connection.onerror = e => {
        console.error('[kernel.worker] handleConnect.error: ', e);
    };
    connection.onmessage = msg => {
        // console.warn('[kernel.worker] onmessage: ');
        let rtn = JSON.parse(msg.data);
        // evaluateUpdates(rtn, subscriptions);
        handleMessage(rtn, subscriptions);
    };
    connection.onclose = () => {
        console.warn('[kernel.worker] websocket closed!');
        postMessage({ task: 'offline', data: Date.now() });
        handleTerminate();
    };
    console.log('[kernel.worker] handleConnect: ', ws, connectTime);

    // reloadTag = false;
    timer = setInterval(() => {
        // connection.send('');
        if (!serverTime && Date.now() - connectTime < 1000 * 10) return;
        // let offlineTime = Date.now() - serverTime;
        let delay = serverTime - lastServerTime;
        if (lifeCyclePeriod && delay > lifeCyclePeriod.value * 2) {
            // console.warn('[worker] socket delay:', delay);
            console.warn(`[worker] lifeCyclePeriod: ${lifeCyclePeriod.value}, socket delay: ${delay}`);
        }
        let offlineTime = Date.now() - lastMessageTime;
        // console.log('watchdog => ', delay, offlineTime);
        // send offline...
        if (lifeCyclePeriod && offlineTime > lifeCyclePeriod.value * 2) {
            console.warn('[worker] socket delay:', offlineTime);
        }
        if (lifeCyclePeriod && offlineTime > 1000 * 10) {
            // handleTerminate();
            postMessage({ task: 'offline', data: offlineTime });
        }
    }, 1000);
}

let lastCycleResources = [];
let totalTag = false;
function handleMessage(res, subscriptions) { //eslint-disable-line
    let resName = res && Object.keys(res).length && Object.keys(res)[0];
    if (!resName) return console.error('[kernel] handleMessage: resource name is null');
    let resValue = res[resName];
    // if (!resValue || !resValue.length) return;
    if (!resValue) return;
    // console.warn('[kernel] handleMessage: ', res, subscriptions, resName);

    if (resName.toLowerCase() === 'ws') {
        // post messages each lifecicle
        if (Object.keys(resources).length) {
            let rtn = {};
            refreshTypes = Object.assign(refreshTypes, reQueueTypes);
            Object.keys(resources).forEach(k => (rtn[k] = refreshTypes[k] ? resources[k] : null));
            reQueueTypes = {};
            if (!totalTag) {
                totalTag = true;
                postMessage({ task: 'resources', data: resources, refreshTypes });
            } else postMessage({ task: 'increment', data: rtn, refreshTypes });
        }
        // clear
        refreshTypes = {}; // 需要刷新的层，精确刷新，提升效率
        handleWsMessage(resValue, JSON.stringify(lastCycleResources).length);
        lastCycleResources = [];
        return;
    } else {
        lastCycleResources.push(res);
    }

    // create & update
    if (!Array.isArray(resources[resName])) resources[resName] = [];
    for (let i = 0, len = resValue.length; i < len; i++) {
        let idx = resources[resName].findIndex(v => v.id === resValue[i].id);
        if (idx < 0) {
            // create
            resources[resName].push(resValue[i]);
            idx = resources[resName].length - 1;
            if (resName === 'contours') contourCreate(idx);
            else if (resName === 'ranges') rangeCreate(idx);
            else if (resName === 'paths') pathCreate(idx);
            else if (resName === 'vehicles') vehicleCreate(idx);
            else if (resName === 'settings') settingCreate(idx);
            else if (resName === 'jobs') jobCreate(idx);
            else if (resName === 'models') modelCreate(idx);
        } else {
            // update
            for (let p in resValue[i]) {
                if (!resValue[i].hasOwnProperty(p)) continue;
                resources[resName][idx][p] = resValue[i][p];
                if (resName === 'contours') contourUpdateProperty(idx, p);
                else if (resName === 'ranges') rangeUpdateProperty(idx, p);
                else if (resName === 'paths') pathUpdateProperty(idx, p);
                else if (resName === 'vehicles') vehicleUpdateProperty(idx, p);
                else if (resName === 'jobs') jobUpdateProperty(idx, p);
                else if (resName === 'models') modelUpdateProperty(idx, p);
            }
        }
    }
    // 精确刷新标识
    refreshTypes[resName] = true;
    if (resName.match(/paths|paths|contours|ranges/)) refreshTypes[resName + 'Labels'] = true;
    if (resName.match(/paths/)) refreshTypes[resName + 'Signs'] = true;

    let len = resources[resName].length;
    while (len--)
        if (!resValue.find(o => o && o.id === resources[resName][len].id)) {
            // let id = resources[listName][len].id;
            resources[resName].splice(len, 1);
            // 精确刷新标识
            refreshTypes[resName] = true;
            if (resName.match(/paths|paths|contours|ranges/)) refreshTypes[resName + 'Labels'] = true;
            if (resName.match(/paths/)) refreshTypes[resName + 'Signs'] = true;
            // if (listName === 'events') console.warn('[kernel] syncList: ', resources[listName]);
        }

    // let rtn = {};
    // refreshTypes = Object.assign(refreshTypes, reQueueTypes);
    // Object.keys(resources).forEach(k => (rtn[k] = refreshTypes[k] ? resources[k] : null));
    // reQueueTypes = {};
    // postMessage({ task: 'increment', data: rtn, refreshTypes });
}
function handleWsMessage(res, len) {
    let now = Date.now();
    lastServerTime = serverTime ? serverTime : res[0];
    lastMessageTime = now;
    serverTime = res[0];
    workTime = res[1];
    let ws = {
        timeDelta: res[0] - now,
        workTime: res[1],
        serverClock: res[2],
        serverTime: res[0]
    };
    // console.log(res, ws);
    postMessage({ task: 'ws', data: { ws, d: len } }); // 服务器时间状态
}

function evaluateUpdates(res, subscriptions) { //eslint-disable-line
    let timePacket = res[0],
        now = Date.now();
    // serverTime = now;
    lastServerTime = serverTime ? serverTime : timePacket[0];
    lastMessageTime = now;
    serverTime = timePacket[0];
    workTime = timePacket[1];
    let ws = {
        timeDelta: timePacket[0] - now,
        workTime: timePacket[1],
        serverClock: timePacket[2],
        serverTime: timePacket[0]
    };
    // console.log('[kernel.worker] evaluateUpdates: ', timePacket, ws);
    postMessage({ task: 'ws', data: { ws, d: JSON.stringify(res).length } }); // 服务器时间状态

    // console.warn('[kernel.worker] evaluateUpdates: ', subscriptions, res);
    refreshTypes = {}; // 需要刷新的层，精确刷新，提升效率
    for (let i = 0; i < subscriptions.length; i++) {
        let list = res[i + 1];
        if (!list) continue; // null means no access or unmodified

        syncList(list, subscriptions[i]);
        // TODO: check it !
        let panic = list.find(v => v.id === 'panic');
        if (panic && panic['value']) postMessage({ task: 'panic', data: list['panic'] });
        // if (panic) console.warn('----> ', subscriptions[i], list);
    }
    let rtn = {};
    refreshTypes = Object.assign(refreshTypes, reQueueTypes);
    Object.keys(resources).forEach(k => (rtn[k] = refreshTypes[k] ? resources[k] : null));
    reQueueTypes = {};
    postMessage({ task: 'increment', data: rtn, refreshTypes });
}
function syncList(list, listName) {
    if (!Array.isArray(resources[listName])) resources[listName] = [];
    // console.warn('[kernel.worker] evaluateUpdates: ', list, listName, refreshTypes);
    for (let i = 0, len = list.length; i < len; i++) {
        let idx = resources[listName].findIndex(v => v.id === list[i].id);
        if (idx < 0) {
            // create
            let o = list[i];
            // if (listName === 'vehicles') o = vehicleCreate(o);
            resources[listName].push(o);
            idx = resources[listName].length - 1;
            if (listName === 'contours') contourCreate(idx);
            else if (listName === 'ranges') rangeCreate(idx);
            else if (listName === 'paths') pathCreate(idx);
            else if (listName === 'vehicles') vehicleCreate(idx);
            else if (listName === 'settings') settingCreate(idx);
            else if (listName === 'jobs') jobCreate(idx);
            else if (listName === 'models') modelCreate(idx);
            // postMessage({ task: 'create', data: list[i], dataKay: listName }); // 增量
            // console.warn(listName, o);
        } else {
            // update
            // if ()
            for (let p in list[i]) {
                if (!list[i].hasOwnProperty(p)) continue;
                resources[listName][idx][p] = list[i][p];
                if (listName === 'contours') contourUpdateProperty(idx, p);
                else if (listName === 'ranges') rangeUpdateProperty(idx, p);
                else if (listName === 'paths') pathUpdateProperty(idx, p);
                else if (listName === 'vehicles') vehicleUpdateProperty(idx, p);
                else if (listName === 'jobs') jobUpdateProperty(idx, p);
                else if (listName === 'models') modelUpdateProperty(idx, p);
            }
            // postMessage({ task: 'update', data: list[i], dataKay: listName, dataIndex: idx }); // 增量
        }
        // TODO：精确刷新，根据返回的数据判断是否需要刷新
        refreshTypes[listName] = true;
        if (listName.match(/paths|paths|contours|ranges/)) refreshTypes[listName + 'Labels'] = true;
        if (listName.match(/paths/)) refreshTypes[listName + 'Signs'] = true;
    }

    let len = resources[listName].length;
    // if (listName === 'events') console.warn('[kernel] syncList: ', len, list);
    // while (index--) if (!Utils.getObject(window[listName][index].id, serverList)) {
    //     var id = window[listName][index].id;
    //     window[listName].splice(index, 1);
    while (len--)
        if (!list.find(o => o && o.id === resources[listName][len].id)) {
            // let id = resources[listName][len].id;
            resources[listName].splice(len, 1);
            refreshTypes[listName] = true;
            if (listName.match(/paths|paths|contours|ranges/)) refreshTypes[listName + 'Labels'] = true;
            if (listName.match(/paths/)) refreshTypes[listName + 'Signs'] = true;
            // if (listName === 'events') console.warn('[kernel] syncList: ', resources[listName]);
        }

    // console.warn(Date.now(), '[kernel.worker] syncList: ', listName, list);
    // console.warn(Date.now(), '[kernel.worker] syncList: ', resources);
    // postMessage({ task: 'resources', data: resources, refreshTypes }); // 全量
}
function handleSend(msg) {
    if (!connection || connection.readyState !== connection.OPEN) return;
    // console.warn('[kernel.worker] send: ', connection, msg);
    connection.send(JSON.stringify(msg));
}

// =================== Utils
function normalize(angle) {
    if (angle > +180) angle -= 360;
    if (angle < -180) angle += 360;
    return angle;
}
function getTimeString(ms, UTC) {
    var date = new Date(ms);

    var h = UTC ? date.getUTCHours() : date.getHours();
    var m = UTC ? date.getUTCMinutes() : date.getMinutes();
    var s = UTC ? date.getUTCSeconds() : date.getSeconds();

    if (h < 10) h = '0' + h;
    if (m < 10) m = '0' + m;
    if (s < 10) s = '0' + s;

    return h + ':' + m + ':' + s;
}
// =================== setting logic
function settingCreate(idx) {
    let o = resources['settings'][idx];
    if (o.id === 'logfileList') {
        resources['logfiles'] = JSON.parse(o.value);
        refreshTypes['logfiles'] = true;
    }
}

// =================== model logic
function modelCreate(idx) {
    let o = resources['models'][idx];
    for (let p in o) {
        if (!o.hasOwnProperty(p)) continue;
        modelUpdateProperty(idx, p);
    }
}
function modelUpdateProperty(idx, property) {
    let o = resources['models'][idx];
    if (property !== 'shape' && property !== 'size' && property !== 'properties') return;
    // console.warn('[kernel.worker] modelUpdateProperty: ', idx, property, o.vehicleKey);
    (resources['vehicles'] || []).forEach((t, i) => {
        if (t.id.startsWith(o.vehicleKey)) vehicleUpdateProperty(i, 'shape');
    });
}
// =================== contour logic
function contourCreate(idx) {
    let o = resources['contours'][idx];
    for (let p in o) {
        if (!o.hasOwnProperty(p)) continue;
        contourUpdateProperty(idx, p);
    }
}
function contourUpdateProperty(idx, property, type = 'contours') {
    let o = resources[type][idx];
    if (property === 'points') {
        // o.pointsExt = this.getPolygonPoints(o);
        o.iPointsExt = [];
        if (o.points && o.points.length)
            o.points.forEach(p => o.iPointsExt.push(Point.getIsometricPos(p.x, p.y, true)));
    } else if (property === 'color') {
        o.fillExt = o.color || 'rgba(0,0,0,0.25)';
        o.strokeStyleExt = Color.lightenDarkenColor(Color.rgbToHex(o.fillExt), 0.5);
        // console.warn('------> ', o.fillExt, Color.rgbToHex(o.fillExt), o.strokeExt);
    } else if (property === 'layer') {
        // if (typeof this.contourLayersHidden[o.layer] === 'undefined') this.contourLayersHidden[o.layer] = false;
    } else if (
        property === 'area' ||
        property === 'center' ||
        property === 'label' ||
        property === 'labelColor' ||
        property === 'labelOffset'
    ) {
        o.strokeExt = 'rgba(0, 255, 255, 0.75)';
        // o.strokeExt = Color.lightenDarkenColor(Color.rgbToHex(o.fillExt), 0.5);
        o.strokeWidthExt = Math.pow(o.area * 1000000, 1 / 3);
        updateLabel(type, idx);
    } else {
        // stroke, when selected
        // let idx = this.contours[o.layer].findIndex(v => v.id === o.id),
        //     len = this.contours[o.layer].length;
        // o.strokeExt = 'rgba(0, 255, 255, 0.75)';
        // if (idx < len - 1) {
        //     // adjust array order to resolve the problem of layer coverage
        //     let o = this.contours[o.layer].splice(idx, 1)[0];
        //     this.contours[o.layer].push(o);
        // }
        // if (idx > 0) {
        //     // adjust array order to resolve the problem of layer coverage
        //     let o = this.contours[o.layer].splice(idx, 1)[0];
        //     this.contours[o.layer].unshift(o);
        // }
    }
}
// =================== range logic
function rangeCreate(idx) {
    let o = resources['ranges'][idx];
    for (let p in o) {
        if (!o.hasOwnProperty(p)) continue;
        rangeUpdateProperty(idx, p);
    }
}
function rangeUpdateProperty(idx, property) {
    contourUpdateProperty(idx, property, 'ranges');
}

// =================== path logic
function pathCreate(idx) {
    let o = resources['paths'][idx];
    for (let p in o) {
        if (!o.hasOwnProperty(p)) continue;
        pathUpdateProperty(idx, p);
    }
}
function pathUpdateProperty(idx, property) {
    let o = resources['paths'][idx];
    if (property === 'width') {
        o.pathStrokeWidthExt = o.width;
        o.markerStrokeWidthExt = o.width;
        o.markerStrokeDasharrayExt = o.width;
        o.markerStrokeDashoffsetExt = o.width * 2;

        o.strokeExt = 'rgba(0, 255, 255, 0.75)';
        o.markerDisplayExt = 'inline';
        updateLabel('paths', idx);
        updatePathSigns(idx);
    } else if (property === 'color') {
        o.pathStrokeExt = o.color || 'rgba(0,0,0,0.25)';
    } else if (property === 'label' || property === 'labelColor' || property === 'labelOffset') {
        updateLabel('paths', idx);
    } else if (property === 'controlPoints') {
        let d = getPathDescription(o.controlPoints);
        o.pathDExt = d;
        o.markerDExt = d;
        updateLabel('paths', idx);
        updatePathSigns(idx);
        o.iControlPointsExt = [];
        if (o.controlPoints && o.controlPoints.length)
            o.controlPoints.forEach(p => o.iControlPointsExt.push(Point.getIsometricPos(p.x, p.y, true)));
        o.iPathDExt = o.iMarkerDExt = getPathDescription(o.iControlPointsExt);
    } else if (property === 'bridges') {
        o.iBridgesExt = [];
        if (o.bridges && o.bridges.length)
            o.bridges.forEach(p => o.iBridgesExt.push(Point.getIsometricPos(p.x, p.y, true)));
    } else if (property === 'properties') {
        updatePathSigns(idx);
    }
}
function getPathDescription(ps = []) {
    let d = 'M' + ps[0].x + ',' + ps[0].y + (ps.length > 2 ? ' C' : ' L');
    for (let i = 1; i < ps.length; i++) d += ps[i].x + ',' + ps[i].y + ' ';
    return d;
}
// const SIGNS = {
//     CLOSURE: 'closure',
//     CONDITION: 'condition',
//     RELATIVESPEED: 'relativeSpeed',
//     STOP: 'stop',
//     SPOTTURN: 'spotTurn',
//     PROHIBITION: 'prohibition',
//     PERMISSION: 'permission'
// };
function updatePathSigns(idx) {
    let o = resources['paths'][idx];
    if (!resources['pathsSigns']) resources['pathsSigns'] = {};

    let activeTypes = [];
    Object.keys(PATH_SIGNS).forEach(s => {
        if (signIsActive(o, PATH_SIGNS[s])) activeTypes.push(PATH_SIGNS[s]);
    });

    Object.keys(resources['pathsSigns']).forEach(k => {
        if (resources['pathsSigns'][k]['pathIdExt'] === o.id) delete resources['pathsSigns'][k];
    });

    let positions = getSignPositions(o, activeTypes);
    activeTypes.forEach((t, i) => {
        let id = t + 'Sign_' + o.id;
        if (!resources['pathsSigns'][id]) resources['pathsSigns'][id] = {};
        let ext = resources['pathsSigns'][id];
        ext.id = id;
        ext.pathIdExt = o.id;
        ext.typeExt = t;
        ext.widthExt = o.width;
        ext.textExt = getSignText(o, t);
        ext.transformExt = null;
        ext.progressExt = positions[i];
        // if (o.id === '1') console.warn(ext);
    });
}
function signIsActive(o, type) {
    if (type === PATH_SIGNS.CLOSURE) return o.properties[type] && o.properties[type] === 'true';
    if (type === PATH_SIGNS.CONDITION) return o.properties[type] && o.properties[type] !== '100';
    if (type === PATH_SIGNS.RELATIVESPEED) return o.properties[type] && o.properties[type] !== '100';
    if (type === PATH_SIGNS.STOP) return o.properties[type] && o.properties[type] !== '-1';
    if (type === PATH_SIGNS.SPOTTURN) return o.properties[type] && o.properties[type] === 'true';
    if (type === PATH_SIGNS.PROHIBITION) return o.properties[type] && o.properties[type] !== 'NONE';
    if (type === PATH_SIGNS.PERMISSION) return o.properties[type] && o.properties[type] !== 'TCV';
    if (type === PATH_SIGNS.BRIDGE) return o.properties['level'] && o.bridges && o.bridges.length;
}
function getSignText(s, type) {
    if (type === 'closure') return null;
    if (type === 'condition') return s.properties[type];
    if (type === 'relativeSpeed') return s.properties[type];
    if (type === 'stop') return null;
    if (type === 'spotTurn') return null;
    if (type === 'prohibition') return s.properties[type].replace('TCV', '');
    if (type === 'permission') return s.properties[type].replace('TCV', '');
}
function getSignPositions(s, activeTypes) {
    let positions = [],
        start = 0,
        end = 100,
        indexOfStop = activeTypes.indexOf('stop');
    if (indexOfStop >= 0) {
        if (s.properties.stop < 50) start = Number(s.properties.stop);
        else end = Number(s.properties.stop);
    }
    let colCount = activeTypes.length + 1 - (indexOfStop >= 0),
        colWidth = (end - start) / colCount,
        count = 0;
    for (let i = 0; i < activeTypes.length; i++) {
        if (i === indexOfStop) positions[i] = s.properties.stop;
        else positions[i] = start + colWidth * ++count;
    }
    return positions;
}
// =================== vehicles logic
function vehicleCreate(idx) {
    // console.warn('[kernel.worker] vehicleCreate: ', Date.now());
    let vehicle = resources['vehicles'][idx];
    if (!vehicle) return;
    let m = (resources['models'] || []).find(m => new RegExp('^' + m.vehicleKey).test(vehicle.id));
    resources['vehicles'][idx].modelExt = m;
    for (let p in vehicle) {
        if (!vehicle.hasOwnProperty(p)) continue;
        vehicleUpdateProperty(idx, p);
    }
    // ext
    if (!vehicle.shapeExt) vehicleUpdateProperty(idx, 'shape');
}

function vehicleUpdateProperty(idx, property) {
    let t = resources['vehicles'][idx];
    // update response time...
    let ms = serverTime - t.lastTelegramTime,
        str = ms + ' ms';
    if (Math.abs(ms) >= 1000) str = Math.floor(ms / 1000) + ' sec';
    if (Math.abs(ms) >= 60000) str = Math.floor(ms / 60000) + ' min';
    if (Math.abs(ms) >= 3600000) str = Math.floor(ms / 3600000) + ' h';
    t.responseExt = str;
    t.jobtimeExt = getTimeString(Math.abs(workTime - t.lastJobTime), true);

    if (property === 'tunedPos') {
        // // 0 to 360 swaps are visible because of css transition
        let angle = (resources.vehicles[idx] && resources.vehicles[idx].angleExt) || 0;
        angle += normalize(normalize(t.tunedPos.h) - (angle % 360));
        t.angleExt = angle;
        if (t.tunedPos && Object.keys(t.tunedPos).length) {
            t.iTunedPosExt = Point.getIsometricPos(t.tunedPos.x, t.tunedPos.y, true);
            t.iTunedPosExt.h = t.tunedPos.h;
        }
    } else if (
        property === 'remoteId' ||
        property === 'isOnline' ||
        property === 'isValidated' ||
        property === 'hasAlert' ||
        property === 'alertColor' ||
        property === 'properties'
    ) {
        let yellow = t.updateIndex >= 0 || t.remoteId !== 'FREE',
            red = !t.isOnline || !t.isValidated || t.properties.stop === 'true';
        // console.log('-----> ', !t.isOnline, !t.isValidated, t.properties.stop === 'true');
        if (yellow || red || t.alertColor.length) {
            // t.blinkerAnimationExt = 'blinker 1s linear infinite';
            t.blinkerFillExt = red
                ? 'rgba(231, 13, 13, 0.8)'
                : t.alertColor.length
                ? t.alertColor
                : 'rgba(227, 216, 15, 0.8)';
        } else {
            t.blinkerFillExt = null;
            t.blinkerAnimationExt = 'none';
        }

        if (t.properties && t.properties.pathId && resources.paths) {
            let lo = resources.paths.find(s => s.id === String(t.properties.pathId));
            t.locationExt = {
                value: lo ? (lo.label ? lo.label : 'ID ' + t.properties.pathId) : 'NONE',
                bg: lo ? lo.labelColor : ''
            };
        }
    } else if (property === 'rawInput') {
        // show or hide parts of the shape according to the status bits
        extendVehicleStatus(t);
    } else if (property === 'envelope') {
        // createEnvelope(...)
        // console.warn('[kernel] ', t.envelope);
    } else if (property === 'route') {
        t.jobInfoExt = 'IDLE';
        t.jobInfoStrokeExt = 'rgba(255,255,255,0.75)';

        if (resources.paths) {
            let deId = t.route ? t.route[t.route.length - 1] : -1,
                de = resources.paths.find(s => s.id === String(deId));
            t.destinationExt = {
                value: de ? (de.label ? de.label : 'ID ' + deId) : 'NONE',
                bg: de ? de.labelColor : ''
            };
        }
    } else if (property === 'shape') {
        let m = (resources['models'] || []).find(m => new RegExp('^' + m.vehicleKey).test(t.id));
        if (!m) return;

        t.outlineXExt = String(-m.size.x / 2);
        t.outlineYExt = String(-m.size.y / 2);
        t.outlineWidthExt = m.size.x;
        t.outlineHeightExt = m.size.y;
        t.outlineStrokeWidthExt = String(Math.min(m.size.x, m.size.y) / 20);

        t.shapeExt = m.shape;

        let coronaRadius = isNaN(Number(m.properties.coronaRadius))
            ? Math.max(m.size.x, m.size.y) * 0.75
            : Number(m.properties.coronaRadius);
        t.blinkerRExt = coronaRadius;
    } else if (property === 'visible') {
        /** custom property, show or hiden some vehicles */
        t.visible = 'visibility';
        // ext.visible = 'visibility';
        // t.visible = t.visible;
    }
}
function extendVehicleStatus(o) {
    if (!o || !o.modelExt) return;
    let model = o.modelExt,
        status = {};

    if (!model || !model.status) return o;
    for (let k in model.status) {
        if (!model.status.hasOwnProperty(k)) return o;
        let sta = model.status[k];
        if (!sta) return o;

        let val = null;
        if (sta.length > 1) {
            val = sta.index < 0 ? o.properties[k] : Expert.getNumberFromByteArray(o.rawInput, sta.index, sta.length);
        } else {
            val = sta.index < 0 ? o.properties[k] === 'true' : Expert.checkBit(o.rawInput, sta.index);
        }

        let color = val ? sta.color : '';

        if (sta.length > 1) {
            if (sta.color === 'auto') color = val < 50 ? (val < 10 ? 'red' : 'yellow') : 'green';
        }

        status[k] = {
            value: val,
            color
        };
        // console.log('=== ', k, val, color, sta);
    }
    o.statusExt = status;
    return o;
}
// =================== jobs logic
function jobCreate(idx) {
    let o = resources['jobs'][idx];
    if (!o) return;
    for (let p in o) {
        if (!o.hasOwnProperty(p)) continue;
        jobUpdateProperty(idx, p);
    }
}
function jobUpdateProperty(idx, property) {
    let o = resources['jobs'][idx];
    if (property === 'progress') {
        o.progressLabelExt = parseFloat(Math.round(o.progress * 10000) / 100).toFixed(2);
        o.progressColorExt = 'hsla(' + o.progressLabelExt + ', 100%, 50%, 0.5)';
    }
}
// =================== labels logic
function updateLabel(typ, idx) {
    if (!typ || typeof idx === 'undefined' || idx < 0) return;
    let o = resources[typ][idx];
    if (!o || !o.label) return;
    if (!resources[typ + 'Labels']) resources[typ + 'Labels'] = {};
    if (!resources[typ + 'Labels'][o.id]) resources[typ + 'Labels'][o.id] = {};
    let ext = resources[typ + 'Labels'][o.id];
    // if (typ === 'ranges') console.warn(resources);

    if (o.center) {
        // if contours
        ext.x = o.center.x;
        ext.y = o.center.y;
    } else if (o.controlPoints) {
        // if paths
        let cp = o.controlPoints[o.controlPoints.length - 1];
        ext.x = cp.x;
        ext.y = cp.y;
    }
    if (ext.x && ext.y) {
        // ext.transformExt = 'translate(' + o.center.x + ',' + o.center.y + ') rotate(' + (-SVG.getRotation(this.svgMatrix)) + ') scale(1 -1)';
        // ext.transformExt = 'translate(' + ext.x + ',' + ext.y + ') rotate(0) scale(1 -1)';
        // ext.transformExt = `translate(${ext.x} ${ext.y}) rotate(${this.rotation}) scale(1 -1)`;
        ext.transformExt = `translate(${ext.x} ${ext.y}) rotate(0) scale(1 -1)`;
        ext.connectorX1Ext = ext.x;
        ext.connectorY1Ext = ext.y;
    }
    if (o.labelOffset && ext.x && ext.y) {
        ext.connectorDisplayExt = o.labelOffset.x === 0 && o.labelOffset.y === 0 ? 'none' : 'inline';
        ext.x += o.labelOffset.x;
        ext.y += o.labelOffset.y;
        ext.transformExt = `translate(${ext.x} ${ext.y}) rotate(0) scale(1 -1)`;
        ext.connectorX2Ext = ext.x;
        ext.connectorY2Ext = ext.y;
    }
    if (o.label) {
        if (o.area) ext.stockWidthExt = Math.pow(o.area * 1000000, 1 / 3);
        else if (o.width) ext.stockWidthExt = o.width;
        else ext.stockWidthExt = 1;
        ext.lineStrokeWidthExt = ext.stockWidthExt * 4;
        ext.textFontSizeExt = ext.stockWidthExt * 2.25;
        ext.connectorStrokeWidthExt = ext.stockWidthExt / 3;
        ext.stringWidthExt = o.label.length * 520;
        let r = ext.lineStrokeWidthExt / 2,
            half = Math.max(1, ext.stringWidthExt / 2 - r / 2);
        ext.lineX1Ext = -half;
        ext.lineX2Ext = +half;
        // if (o.id === '114') console.warn('@@@ p-sitemap-paths.upateLabel: ', r, half, w);
    }
    if (o.labelColor) {
        ext.lineStrokeExt = o.labelColor;
        ext.connectorStrokeColorExt = o.labelColor;

        // ext.textFillExt = 'rgba(0, 0, 0, 0.75)';
        ext.textFillExt = Color.getContrastColor(o.labelColor);
    }

    ext.textExt = o.label;
    if (ext.textExt) ext.transitionExt = 'all 500ms';
    else ext.transitionExt = 'none';

    let ip = Point.getIsometricPos(ext.x, ext.y, true);
    ext.ix = ip.x;
    ext.iy = ip.y;
}
